﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using Sharebert.OverlayLibrary.API;
using Sharebert.LinkLayerLibrary.API;
using Sharebert.LinkLayerLibrary;
using System.Threading;
using System.Timers;
using System.Net.Sockets;
using System.Net;
using Sharebert.OverlayLibrary.Messages;
using log4net;

namespace Sharebert.OverlayLibrary
{
    /// <summary>
    /// BootstrapClient is used to communicate with the bootstrap server. After sending our PeerID to the bootstrap server,
    /// it will return with our predecessor and successor.
    /// By default, the bootstrap client will automatically ping the server.
    /// </summary>
    public class BootstrapClient : IBootstrapClient
    {
	private static readonly ILog logger = LogManager.GetLogger(typeof(BootstrapClient));

        private const int DEFAULT_BOOTSTRAP_PING_INTERVAL = 30;

        private const int DEFAULT_READ_TIMEOUT = 30;

        private const int DEFAULT_WRITE_TIMEOUT = 10;

        /// <summary>
        /// Initializes a new instance of the BootstrapClient class. A random port greater than 1023 is used to listen for new peer connections.
        /// </summary>
        /// <param name="serverAddress">Address of the bootstrap server</param>
        /// <param name="serverPort">Port of the bootstrap server</param>
        public BootstrapClient(string serverAddress, int serverPort)
        {
            this.ServerAddress = serverAddress;
            this.ServerPort = serverPort;
            this.connection = new Connection();
            this.connection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            this.connection.ConnectionClosed += new EventHandler<EventArgs>(HandleConnectionClosed);
            this.connection.ConnectionTimedOut += new EventHandler<EventArgs>(HandleConnectionTimedOut);

            this.ServerPingInverval = DEFAULT_BOOTSTRAP_PING_INTERVAL;
            this.AutoServerPingEnabled = true;
            this.bootstrapPingTimer = new System.Timers.Timer();
            this.bootstrapPingTimer.Elapsed += new System.Timers.ElapsedEventHandler(HandleBootstrapTimerElapsed);

            this.ReadTimeout = DEFAULT_READ_TIMEOUT;
            this.WriteTimeout = DEFAULT_WRITE_TIMEOUT;
        }

        /// <summary>
        /// Requests the predecessor and successor for the calculated peer ID from the bootstrap server.
        /// </summary>
        /// <param name="peer">The peer object</param>
        /// <exception cref="NetworkException">Could not connect or sending message failed</exception>
        public void RequestNeighbours(IPeer peer)
        {
            this.localPeer = peer;
            var payload = Serialisation.Serialize(peer);
            var header = new Header(MessageTypes.NEIGHBOUR_REQUEST, payload.Length);
            var message = new Message(header, payload);

            try
            {
                lock (this.lockObj)
                {
                    StopBootstrapPingTimer();
                    if (!this.connection.Connected)
                    {
                        this.connection.Open(this.ServerAddress, this.ServerPort);
                    }
                    this.connection.SendMessage(message);
                    logger.Debug("Send neighbour request to bootstrap server");
                }
            }
            catch (Exception ex)
            {
                this.connection.Close();
                throw ex;
            }
        }

        /// <summary>
        /// Sends a ping message to the server to signal that the peer with the given ID is still alive.
        /// </summary>
        /// <exception cref="NetworkException">Could not connect or sending message failed</exception>
        private void PingBootstrapServer()
        {
            if (this.localPeer == null)
            {
                // no request was done before
                return;
            }
            var payload = Serialisation.Serialize(this.localPeer);
            var header = new Header(MessageTypes.PING, payload.Length);
            var message = new Message(header, payload);

            try
            {
                this.connection.Open(this.ServerAddress, this.ServerPort);
                this.connection.SendMessage(message);
                logger.Debug("Send Ping message to bootstrap server");
            }
            finally
            {
                this.connection.Close();
            }
        }

        /// <summary>
        /// Peer is going offline and tells the bootstrap server to remove him from the list.
        /// </summary>
        /// <exception cref="NetworkException">Could not connect to server or send message</exception>
        public void Offline()
        {
            if (this.localPeer == null)
            {
                // no request was done before
                return;
            }
            var payload = Serialisation.Serialize(this.localPeer);
            var header = new Header(MessageTypes.OFFLINE, payload.Length);
            var message = new Message(header, payload);

            try
            {
                lock (this.lockObj)
                {
                    StopBootstrapPingTimer();
                    this.connection.Open(this.ServerAddress, this.ServerPort);
                    this.connection.SendMessage(message);
                }
            }
            finally
            {
                this.connection.Close();
            }
        }

        private void HandleMessageReceived(object sender, MessageReceivedEventArgs e)
        {
            if (e.Message.Header.Command.Equals(MessageTypes.NEIGHBOUR_RESPONSE))
            {
                try
                {
                    if (this.NeighboursReceived != null)
                    {
                        var neighbours = (NeighbourList)Serialisation.Deserialize(e.Message.Payload, typeof(NeighbourList));

                        this.NeighboursReceived.Invoke(this, new NeighboursReceivedEventArgs(neighbours, this.connection.LocalAddress));
                    }
                    lock (this.lockObj)
                    {
                        StartBootstrapPingTimer();
                    }
                }
                catch (SerializationException ex)
                {
                    logger.Error("Deserialization failed!", ex);
                }
                finally
                {
                    this.connection.Close();
                }
            }
            else if (e.Message.Header.Command.Equals(MessageTypes.PEERID_IN_USE))
            {
                if (this.PeerIdInUseReceived != null)
                {
                    this.PeerIdInUseReceived.Invoke(this, new EventArgs());
                }
                else
                {
                    logger.Error("Not listening!");
                }
            }
        }

        private void HandleConnectionClosed(object sender, EventArgs e)
        {
            if (this.ConnectionLost != null)
            {
                this.ConnectionLost.Invoke(this, new EventArgs());
            }
            this.connection.Close();
        }

        private void HandleConnectionTimedOut(object sender, EventArgs e)
        {
            if (this.ConnectionLost != null)
            {
                this.ConnectionLost.Invoke(this, new EventArgs());
            }
            this.connection.Close();
        }


        private void HandleBootstrapTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            try
            {
                lock (this.lockObj)
                {
                    StopBootstrapPingTimer();
                    PingBootstrapServer(); ;
                    StartBootstrapPingTimer();
                }
            }
            catch (Exception ex)
            {
                logger.Error("Failed pinging bootstrap server!", ex);
            }
        }

        private void StartBootstrapPingTimer()
        {
            if (!this.bootstrapPingTimer.Enabled && this.AutoServerPingEnabled)
            {
                this.bootstrapPingTimer.Interval = this.ServerPingInverval * 1000;
                this.bootstrapPingTimer.Start();
            }
        }

        private void StopBootstrapPingTimer()
        {
            if (this.bootstrapPingTimer.Enabled)
            {
                this.bootstrapPingTimer.Stop();
            }
        }


        #region Properties and private member

        /// <summary>
        /// The hostname or IP of the bootstrap server.
        /// </summary>
        public string ServerAddress { get; private set; }

        /// <summary>
        /// The port of the bootstrap server.
        /// </summary>
        public int ServerPort { get; private set; }

        /// <summary>
        /// Time in seconds, the peer will ping the bootstrap server.
        /// </summary>
        public int ServerPingInverval { get; set; }

        /// <summary>
        /// Enables automatic server pinging.
        /// </summary>
        public bool AutoServerPingEnabled { get; set; }

        /// <summary>
        /// Receiving messages will timeout after the specified time in seconds.
        /// </summary>
        public int ReadTimeout
        {
            get { return this.connection.ReadTimeout; }
            set { this.connection.ReadTimeout = value; }
        }

        /// <summary>
        /// Sending messages will timeout after the specified time in seconds.
        /// </summary>
        public int WriteTimeout
        {
            get { return this.connection.WriteTimeout; }
            set { this.connection.WriteTimeout = value; }
        }

        private IConnection connection;

        private IPeer localPeer;

        private System.Timers.Timer bootstrapPingTimer;

        private object lockObj = new object();

        #endregion


        #region Events

        /// <summary>
        /// The server did not respond after Timeout seconds.
        /// </summary>
        public event EventHandler<EventArgs> ConnectionLost;

        /// <summary>
        /// Received the predecessor and successor from the server.
        /// </summary>
        public event EventHandler<NeighboursReceivedEventArgs> NeighboursReceived;

        /// <summary>
        /// The bootstrap server signals, that the choosen PeerID is already in use.
        /// </summary>
        public event EventHandler<EventArgs> PeerIdInUseReceived;

        #endregion

    }
}
